/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.events.aggr.dao.jpa;

import com.google.common.base.Function;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import org.apereo.portal.events.aggr.DateDimension;
import org.apereo.portal.events.aggr.dao.DateDimensionDao;
import org.apereo.portal.jpa.BaseAggrEventsJpaDao;
import org.apereo.portal.jpa.OpenEntityManager;
import org.joda.time.DateMidnight;
import org.joda.time.LocalDate;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.stereotype.Repository;

/** */
@Repository
public class JpaDateDimensionDao extends BaseAggrEventsJpaDao implements DateDimensionDao {
    private CriteriaQuery<DateDimensionImpl> findAllDateDimensionsQuery;
    private CriteriaQuery<DateDimensionImpl> findAllDateDimensionsBetweenQuery;
    private CriteriaQuery<DateDimensionImpl> findAllDateDimensionsWithoutTermQuery;
    private CriteriaQuery<DateDimensionImpl> findNewestDateDimensionQuery;
    private CriteriaQuery<DateDimensionImpl> findOldestDateDimensionQuery;
    private ParameterExpression<LocalDate> dateTimeParameter;
    private ParameterExpression<LocalDate> endDateTimeParameter;

    @Override
    public void afterPropertiesSet() throws Exception {
        this.dateTimeParameter = this.createParameterExpression(LocalDate.class, "dateTime");
        this.endDateTimeParameter = this.createParameterExpression(LocalDate.class, "endDateTime");

        this.findAllDateDimensionsQuery =
                this.createCriteriaQuery(
                        new Function<CriteriaBuilder, CriteriaQuery<DateDimensionImpl>>() {
                            @Override
                            public CriteriaQuery<DateDimensionImpl> apply(CriteriaBuilder cb) {
                                final CriteriaQuery<DateDimensionImpl> criteriaQuery =
                                        cb.createQuery(DateDimensionImpl.class);
                                final Root<DateDimensionImpl> dimensionRoot =
                                        criteriaQuery.from(DateDimensionImpl.class);
                                criteriaQuery.orderBy(
                                        cb.asc(dimensionRoot.get(DateDimensionImpl_.date)));
                                return criteriaQuery;
                            }
                        });

        this.findAllDateDimensionsBetweenQuery =
                this.createCriteriaQuery(
                        new Function<CriteriaBuilder, CriteriaQuery<DateDimensionImpl>>() {
                            @Override
                            public CriteriaQuery<DateDimensionImpl> apply(CriteriaBuilder cb) {
                                final CriteriaQuery<DateDimensionImpl> criteriaQuery =
                                        cb.createQuery(DateDimensionImpl.class);
                                final Root<DateDimensionImpl> dimensionRoot =
                                        criteriaQuery.from(DateDimensionImpl.class);
                                criteriaQuery.select(dimensionRoot);
                                criteriaQuery.where(
                                        cb.and(
                                                cb.greaterThanOrEqualTo(
                                                        dimensionRoot.get(DateDimensionImpl_.date),
                                                        dateTimeParameter),
                                                cb.lessThan(
                                                        dimensionRoot.get(DateDimensionImpl_.date),
                                                        endDateTimeParameter)));
                                criteriaQuery.orderBy(
                                        cb.asc(dimensionRoot.get(DateDimensionImpl_.date)));

                                return criteriaQuery;
                            }
                        });

        this.findAllDateDimensionsWithoutTermQuery =
                this.createCriteriaQuery(
                        new Function<CriteriaBuilder, CriteriaQuery<DateDimensionImpl>>() {
                            @Override
                            public CriteriaQuery<DateDimensionImpl> apply(CriteriaBuilder cb) {
                                final CriteriaQuery<DateDimensionImpl> criteriaQuery =
                                        cb.createQuery(DateDimensionImpl.class);
                                final Root<DateDimensionImpl> dimensionRoot =
                                        criteriaQuery.from(DateDimensionImpl.class);
                                criteriaQuery.select(dimensionRoot);
                                criteriaQuery.where(
                                        cb.isNull(dimensionRoot.get(DateDimensionImpl_.term)));

                                return criteriaQuery;
                            }
                        });

        this.findNewestDateDimensionQuery =
                this.createCriteriaQuery(
                        new Function<CriteriaBuilder, CriteriaQuery<DateDimensionImpl>>() {
                            @Override
                            public CriteriaQuery<DateDimensionImpl> apply(CriteriaBuilder cb) {
                                final CriteriaQuery<DateDimensionImpl> criteriaQuery =
                                        cb.createQuery(DateDimensionImpl.class);
                                final Root<DateDimensionImpl> dimensionRoot =
                                        criteriaQuery.from(DateDimensionImpl.class);

                                // Build subquery for max date
                                final Subquery<LocalDate> maxDateSub =
                                        criteriaQuery.subquery(LocalDate.class);
                                final Root<DateDimensionImpl> maxDateDimensionSub =
                                        maxDateSub.from(DateDimensionImpl.class);
                                maxDateSub.select(
                                        cb.greatest(
                                                maxDateDimensionSub.get(DateDimensionImpl_.date)));

                                // Get the date dimension
                                criteriaQuery
                                        .select(dimensionRoot)
                                        .where(
                                                cb.equal(
                                                        dimensionRoot.get(DateDimensionImpl_.date),
                                                        maxDateSub));

                                return criteriaQuery;
                            }
                        });

        this.findOldestDateDimensionQuery =
                this.createCriteriaQuery(
                        new Function<CriteriaBuilder, CriteriaQuery<DateDimensionImpl>>() {
                            @Override
                            public CriteriaQuery<DateDimensionImpl> apply(CriteriaBuilder cb) {
                                final CriteriaQuery<DateDimensionImpl> criteriaQuery =
                                        cb.createQuery(DateDimensionImpl.class);
                                final Root<DateDimensionImpl> dimensionRoot =
                                        criteriaQuery.from(DateDimensionImpl.class);

                                // Build subquery for max date
                                final Subquery<LocalDate> maxDateSub =
                                        criteriaQuery.subquery(LocalDate.class);
                                final Root<DateDimensionImpl> maxDateDimensionSub =
                                        maxDateSub.from(DateDimensionImpl.class);
                                maxDateSub.select(
                                        cb.least(maxDateDimensionSub.get(DateDimensionImpl_.date)));

                                // Get the date dimension
                                criteriaQuery
                                        .select(dimensionRoot)
                                        .where(
                                                cb.equal(
                                                        dimensionRoot.get(DateDimensionImpl_.date),
                                                        maxDateSub));

                                return criteriaQuery;
                            }
                        });
    }

    @Override
    public DateDimension getNewestDateDimension() {
        final TypedQuery<DateDimensionImpl> query =
                this.createCachedQuery(this.findNewestDateDimensionQuery);
        final List<DateDimensionImpl> resultList = query.getResultList();
        return DataAccessUtils.uniqueResult(resultList);
    }

    @Override
    public DateDimension getOldestDateDimension() {
        final TypedQuery<DateDimensionImpl> query =
                this.createCachedQuery(this.findOldestDateDimensionQuery);
        final List<DateDimensionImpl> resultList = query.getResultList();
        return DataAccessUtils.uniqueResult(resultList);
    }

    @Override
    @AggrEventsTransactional
    public DateDimension createDateDimension(DateMidnight date, int quarter, String term) {
        final DateDimension dateDimension = new DateDimensionImpl(date, quarter, term);

        this.getEntityManager().persist(dateDimension);

        return dateDimension;
    }

    @Override
    @AggrEventsTransactional
    public void updateDateDimension(DateDimension dateDimension) {
        this.getEntityManager().persist(dateDimension);
    }

    @Override
    public List<DateDimension> getDateDimensions() {
        final TypedQuery<DateDimensionImpl> query =
                this.createCachedQuery(this.findAllDateDimensionsQuery);
        query.setFlushMode(FlushModeType.COMMIT);

        final List<DateDimensionImpl> portletDefinitions = query.getResultList();
        return new ArrayList<DateDimension>(portletDefinitions);
    }

    @Override
    public List<DateDimension> getDateDimensionsBetween(DateMidnight start, DateMidnight end) {
        final TypedQuery<DateDimensionImpl> query =
                this.createCachedQuery(this.findAllDateDimensionsBetweenQuery);
        query.setFlushMode(FlushModeType.COMMIT);
        query.setParameter(this.dateTimeParameter, start.toLocalDate());
        query.setParameter(this.endDateTimeParameter, end.toLocalDate());

        final List<DateDimensionImpl> portletDefinitions = query.getResultList();
        return new ArrayList<DateDimension>(portletDefinitions);
    }

    @Override
    public List<DateDimension> getDateDimensionsWithoutTerm() {
        final TypedQuery<DateDimensionImpl> query =
                this.createQuery(this.findAllDateDimensionsWithoutTermQuery);
        query.setFlushMode(FlushModeType.COMMIT);

        final List<DateDimensionImpl> portletDefinitions = query.getResultList();
        return new ArrayList<DateDimension>(portletDefinitions);
    }

    @Override
    public DateDimension getDateDimensionById(long key) {
        final EntityManager entityManager = this.getEntityManager();
        final FlushModeType flushMode = entityManager.getFlushMode();
        try {
            entityManager.setFlushMode(FlushModeType.COMMIT);
            return entityManager.find(DateDimensionImpl.class, key);
        } finally {
            entityManager.setFlushMode(flushMode);
        }
    }

    @OpenEntityManager(unitName = BaseAggrEventsJpaDao.PERSISTENCE_UNIT_NAME)
    @Override
    public DateDimension getDateDimensionByDate(DateMidnight date) {
        final NaturalIdQuery<DateDimensionImpl> query =
                this.createNaturalIdQuery(DateDimensionImpl.class);
        query.using(DateDimensionImpl_.date, date.toLocalDate());
        return query.load();
    }
}
